<?php
/**
 * @package Curly
 * @subpackage Cookies
 * @version 0.9
 * @link http://curly.codeplex.com/
 * @license http://curly.codeplex.com/license The MIT License
 * @author Dawid Zawada
 */

namespace Curly\Cookies
{
    /**
     * A class representing HTTP cookie with all its parameters
     * 
     * The class contains methods to:
     * - indicate whether the cookie should be sent acording to the request URI
     * - convert the cookie from and to its string representation
     * 
     * @package Curly
     * @subpackage Cookies
     * @license http://curly.codeplex.com/license The MIT License  
     */
    class Cookie
    {
        /**
         * Cookie name
         * 
         * @var string
         */
        protected $name;
        
        /** 
         * Cookie value
         * 
         * @var string
         */
        protected $value;
        
        /**
         * Cookie expiry date
         * 
         * @var int
         */
        protected $expiryTime;
        
        /**
         * Cookie path
         * 
         * @var string
         */
        protected $path = "";
        
        /**
         * Cookie domain
         * 
         * @var string
         */
        protected $domain = "";
        
        /**
         * Indicates whether the cookie is secure or not
         * 
         * @var bool
         */
        protected $secure = false;
        
        /**
         * Creates new cookie object with the given name and value
         * 
         * Name of the cookie cannot be empty and cannot contain illegal characters
         * 
         * @param string $name
         * @param string $value [optional]
         * @throws \InvalidArgumentException 
         */
        public function __construct( $name, $value = "" )
        {
            $this->validateCookieName( $name );
            
            $this-> setName( $name );
            $this->setValue( $value );
        }
        
        /**
         * Throws exception if the name of the cookie is empty or
         * contains illegal characters
         * 
         * @param string $name
         * @throws \InvalidArgumentException
         */
        protected function validateCookieName( $name )
        {
            if( empty( $name ) )
            {
                throw new \InvalidArgumentException( "Cookie name cannot be empty" );
            }
            if( preg_match( "~[=,; \t\r\n\013\014]~", $name ) )
            {
                throw new \InvalidArgumentException( "Cookie name ({$name}) contains one or more illegal characters: =,; \\t\\r\\n\\013\\014" );
            }
        }
        
        /**
         * Returns the cookie name
         *
         * @return string
         */
        public function getName()
        {
            return $this->name;
        }
        
        /**
         * Sets the given cookie name
         * 
         * @param string $name
         */
        protected function setName( $name )
        {
            $this->name = (string)$name;
        }
        
        /**
         * Returns the cookie value
         *
         * @return string
         */
        public function getValue()
        {
            return $this->value;
        }
        
        /**
         * Sets the cookie value
         * 
         * @param string $value
         */
        public function setValue( $value )
        {
            $this->value = (string)$value;
        }
        
        /**
         * Returns the cookie expiry time
         *
         * @return int
         */
        public function getExpiryTime()
        {
            return $this->expiryTime;
        }
        
        /**
         * Sets the cookie expiry time
         * 
         * The expiry time parameter must be either a \DateTime object, an integer or null
         * 
         * @param mixed $expiryTime
         */
        public function setExpiryTime( $expiryTime )
        {
            if( $expiryTime instanceof \DateTime )
            {
                $expiryTime = $expiryTime->format( "U" );
            }
            else if( $expiryTime !== null )
            {
                $expiryTime = (int)$expiryTime;
            }
            
            $this->expiryTime = $expiryTime;
        }
        
        /**
         * Returns the cookie path
         *
         * @return string 
         */
        public function getPath()
        {
            return $this->path;
        }
        
        /**
         * Sets the cookie path
         * 
         * @param string $path
         */
        public function setPath( $path )
        {
            $this->path = (string)$path;
        }
        
        /**
         * Returns the cookie domain
         * 
         * @return string
         */
        public function getDomain()
        {
            return $this->domain;
        }
        
        /**
         * Sets the cookie domain
         * 
         * @param string $domain
         */
        public function setDomain( $domain )
        {
            $this->domain = (string)$domain;
        }
        
        /**
         * Indicates whether the cookie should only be sent 
         * over secure connections
         *
         * @return bool
         */
        public function isSecure()
        {
            return $this->secure;
        }
        
        /**
         * Sets an indication of whether the cookie should only be sent 
         * over secure connections
         * 
         * @param bool $secure
         */
        public function setSecure( $secure )
        {
            $this->secure = (bool)$secure;
        }
        
        /**
         * Determines whether the cookie has expired according to the current
         * time or the time passed by a parameter
         * 
         * The time parameter must be either a \DateTime object, an integer or null
         * 
         * @param mixed $time [optional]
         * @return bool
         */
        public function isExpired( $time = null )
        {
            if( is_null( $time ) )
            {
                $time = time();
            }
            else if( $time instanceof \DateTime )
            {
                $time = $time->format( "U" );
            }
            else
            {
                $time = (int)$time;
            }
            
            $expiryTime = $this->getExpiryTime();
            return is_int( $expiryTime ) && $expiryTime < $time;
        }
        
        /**
         * Determines whether both this instance and the given Cookie object 
         * have the same name, path and domain
         * 
         * @param Curly\Cookies\Cookie $cookie
         * @return bool
         */
        public function equals( Cookie $cookie )
        {
            return $cookie->getName() == $this->getName() &&
                   $cookie->getPath() == $this->getPath() &&
                   $cookie->getDomain() == $this->getDomain();
        }
        
        /**
         * Returns a string representation of the cookie
         * 
         * The value of the cookie in the returned sting is encoded
         * if the methods argument is set to true.
         * 
         * Format of the returned string is the same as in a HTTP 
         * request header, but without the 'Cookie: ' prefix.
         * 
         * @param bool $encodeValue [optional]
         * @return string
         */
        public function toString( $encodeValue = true )
        {
            $value = $this->getValue();
            if( $encodeValue )
            {
                $value = rawurldecode( $value );
            }

            return $this->getName() . "={$value}";
        }
        
        /**
         * Returns a string representation of the cookie 
         * 
         * Format is the same as in a HTTP request header, 
         * but without the 'Cookie: ' prefix.
         * The value of the cookie in the returned sting is encoded.
         * 
         * @return string
         */
        public function __toString()
        {
            return $this->toString( true );
        }
        
        /**
         * Determines whether the cookie should be sent in a request 
         * to the given URI
         * 
         * @param string $uri
         * @return bool
         */
        public function matchesUri( $uri )
        {
            $uriParts = parse_url( $uri );
            
            return $this->matchesPath( $uriParts ) &&
                   $this->matchesDomain( $uriParts ) &&
                   $this->matchesScheme( $uriParts );
        }
        
        /**
         * Determines whether the cookies path matches the URIs path
         * 
         * The $uriParts parameter must be a result array of the parse_url function
         * 
         * @param array $uriParts
         * @return bool
         */
        protected function matchesPath( array $uriParts )
        {
            if( empty( $uriParts["path"] ) || $this->getPath() == "" )
            {
                return true;
            }
            else
            {
                return strpos( $uriParts["path"], $this->getPath() ) === 0;
            }
        }
        
        /**
         * Determines whether the cookies domain matches the URIs domain
         * 
         * The $uriParts parameter must be a result array of the parse_url function
         * 
         * @param array $uriParts
         * @return bool
         */
        protected function matchesDomain( array $uriParts )
        {
            if( empty( $uriParts["host"] ) || $this->getDomain() == "" )
            {
                return true;
            }
            else
            {
                $domain = preg_quote( $this->getDomain(), "~" );
                if( strpos( $domain, "\\." ) === 0 )
                {
                    $domain = substr_replace( $domain, "(?:.+\\.|)", 0, 2 );
                }
                
                return preg_match( "~^{$domain}$~i", $uriParts["host"] );
            }
        }
        
        /**
         * Determines whether the cookies secure indication matches the URIs scheme
         * 
         * The $uriParts parameter must be a result array of the parse_url function
         * 
         * @param array $uriParts
         * @return bool
         */
        protected function matchesScheme( array $uriParts )
        {
            if( empty( $uriParts["scheme"] ) )
            {
                return true;
            }
            else
            {
                return !( $this->isSecure() && $uriParts["scheme"] == "http" ); 
            }
        }
        
        /**
         * Creates a cookie object from the given cookie string
         * 
         * Format of the string should be the same 
         * as in a 'Set-Cookie' header of a HTTP response.
         * The method returns null if the cookie string format is incorrect
         * 
         * @param string $cookieStr
         * @param boolean $valueIsEncoded [optional]
         * @return Curly\Cookies\Cookie
         */
        public static function parse( $cookieStr, $valueIsEncoded = true )
        {
            $cookieStr = trim( $cookieStr );
            if( preg_match( "~^(?:Set-Cookie:\\s*)?(?<name>.+?)(?:=|;|$)(?<value>.*?)(?:;|$)\\s*(?<attributes>.*)~i", $cookieStr, $match ) )
            {
                if( $valueIsEncoded )
                {
                    $match["value"] = rawurldecode( $match["value"] );
                }
                
                $cookie = new Cookie( $match["name"], $match["value"] );
                static::setCookieAttributes( $cookie, $match["attributes"] );               
                
                return $cookie;
            }
            else
            {
                return null;
            }
        }
        
        /**
         * Sets the cookie object properties basing on the attributes 
         * extracted from the given part of the cookie string
         * 
         * @param Curly\Cookies\Cookie $cookie
         * @param string $attributesStr
         */
        protected static function setCookieAttributes( Cookie $cookie, $attributesStr )
        {
            $attributes = static::extractAttributesFromString( $attributesStr );
            foreach( $attributes as $attribute )
            {
                switch( $attribute["name"] )
                {
                    case "expires": $cookie->setExpiryTime( new \DateTime( $attribute["value"] ) ); break;
                    case "path": $cookie->setPath( $attribute["value"] ); break;
                    case "domain": $cookie->setDomain( $attribute["value"] ); break;
                    case "secure": $cookie->setSecure( $attribute["value"] ); break;
                }
            }
        }
        
        /**
         * Extracts atributes names and values from the given part 
         * of the cookie string and returns them as an array.
         * 
         * @param string $attributesStr
         * @return array
         */
        protected static function extractAttributesFromString( $attributesStr )
        {
            $attributes = preg_split( "~;\\s*~", $attributesStr );
            array_walk( $attributes, function( &$attribute ) {
                $nameValuePair = explode( "=", $attribute );
                if( count( $nameValuePair ) == 1 )
                {
                    $nameValuePair[] = true;
                }
                
                $attribute = array();
                $attribute["name"] = strtolower( $nameValuePair[0] );
                $attribute["value"] = $nameValuePair[1];
            } );
            
            return $attributes;
        }
    }
}